using System;
using System.Collections.Generic;

namespace PathX.Collection
{
    /// <summary>Represents a matrix of items.</summary>
    /// <typeparam name="T">The item type</typeparam>
    public class Matrix<T>
    {
        private T[,] _matrix;
        private int _columns;
        private int _rows;

        /// <summary>
        /// Initializes a new instance of the <see cref="T:Apex.DataStructures.Matrix`1" /> class.
        /// </summary>
        /// <param name="matrix">The matrix.</param>
        public Matrix(T[,] matrix)
        {
            this._matrix = matrix;
            this._columns = this._matrix.GetUpperBound(0) + 1;
            this._rows = this._matrix.GetUpperBound(1) + 1;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:Apex.DataStructures.Matrix`1" /> class.
        /// </summary>
        /// <param name="columns">The number of columns in the matrix.</param>
        /// <param name="rows">The number of rows in the matrix.</param>
        public Matrix(int columns, int rows)
        {
            this._columns = columns;
            this._rows = rows;
            this._matrix = new T[this._columns, this._rows];
        }

        /// <summary>Gets the number of columns</summary>
        /// <value>The number of columns</value>
        public int columns => this._columns;

        /// <summary>Gets the number of rows</summary>
        /// <value>The number of rows</value>
        public int rows => this._rows;

        /// <summary>
        /// Gets the raw matrix. No bounds checking will be done on access
        /// </summary>
        /// <value>The raw matrix.</value>
        public T[,] rawMatrix => this._matrix;

        /// <summary>Gets the items in the matrix</summary>
        /// <value>The items.</value>
        public IEnumerable<T> items
        {
            get
            {
                for (int x = 0; x < this._columns; ++x)
                {
                    for (int z = 0; z < this._rows; ++z)
                        yield return this._matrix[x, z];
                }
            }
        }

        /// <summary>
        /// Gets the item at the specified position in the matrix. Bounds check is performed and will return null if out of bounds.
        /// </summary>
        /// <param name="column">The column.</param>
        /// <param name="row">The row.</param>
        /// <returns>If the position is valid (in bounds) the item at that position is returned. Otherwise null is returned.</returns>
        public T this[int column, int row] => this.InBounds(column, row) ? this._matrix[column, row] : default(T);

        /// <summary>
        /// Gets a layer of items around a center. A layer is defined as the outer neighbours of the concentric square given by the cellDistance argument.
        /// Distance of 0 is the item itself, Distance of 1 is the 8 neighbouring items, Distance of 2 is the 16  outer most neighboring items to layer 1, etc. Think onion.
        /// </summary>
        /// <param name="column">The column from which to find neighbours</param>
        /// <param name="row">The row from which to find neighbours</param>
        /// <param name="cellDistance">The cell distance, 0 being the item itself.</param>
        /// <param name="neighbours">A dynamic array to populate with the found neighbour cells.</param>
        /// <returns>The list of items making up the layer.</returns>
        public void GetConcentricNeighbours(
            int column,
            int row,
            int cellDistance,
            DynamicArray<T> neighbours)
        {
            if (cellDistance < 0)
                return;
            if (cellDistance == 0)
            {
                neighbours.Add(this[column, row]);
            }
            else
            {
                int x1 = column - cellDistance;
                int x2 = column + cellDistance;
                int index1 = row - cellDistance;
                int index2 = row + cellDistance;
                int bounds1 = this.AdjustColumnToBounds(x1);
                int bounds2 = this.AdjustColumnToBounds(x2);
                int bounds3 = this.AdjustRowToBounds(index1 + 1);
                int bounds4 = this.AdjustRowToBounds(index2 - 1);
                for (int index3 = bounds1; index3 <= bounds2; ++index3)
                {
                    if (index1 >= 0)
                        neighbours.Add(this._matrix[index3, index1]);
                    if (index2 < this._rows)
                        neighbours.Add(this._matrix[index3, index2]);
                }
                for (int index4 = bounds3; index4 <= bounds4; ++index4)
                {
                    if (x1 >= 0)
                        neighbours.Add(this._matrix[x1, index4]);
                    if (x2 < this._columns)
                        neighbours.Add(this._matrix[x2, index4]);
                }
            }
        }

        /// <summary>
        /// Gets a layer of items around a center. A layer is defined as the outer neighbours of the concentric square given by the cellDistance argument.
        /// Distance of 0 is the item itself, Distance of 1 is the 8 neighbouring items, Distance of 2 is the 16  outer most neighboring items to layer 1, etc. Think onion.
        /// </summary>
        /// <param name="column">The column from which to find neighbours</param>
        /// <param name="row">The row from which to find neighbours</param>
        /// <param name="cellDistance">The cell distance, 0 being the item itself.</param>
        /// <returns>The list of items making up the layer.</returns>
        public IEnumerable<T> GetConcentricNeighbours(int column, int row, int cellDistance)
        {
            if (cellDistance >= 0)
            {
                if (cellDistance == 0)
                {
                    yield return this[column, row];
                }
                else
                {
                    int startColumnRaw = column - cellDistance;
                    int endColumnRaw = column + cellDistance;
                    int startRowRaw = row - cellDistance;
                    int endRowRaw = row + cellDistance;
                    int startColumnBounded = this.AdjustColumnToBounds(startColumnRaw);
                    int endColumnBounded = this.AdjustColumnToBounds(endColumnRaw);
                    int startRowBounded = this.AdjustRowToBounds(startRowRaw + 1);
                    int endRowBounded = this.AdjustRowToBounds(endRowRaw - 1);
                    for (int x = startColumnBounded; x <= endColumnBounded; ++x)
                    {
                        if (startRowRaw >= 0)
                            yield return this._matrix[x, startRowRaw];
                        if (endRowRaw < this._rows)
                            yield return this._matrix[x, endRowRaw];
                    }
                    for (int z = startRowBounded; z <= endRowBounded; ++z)
                    {
                        if (startColumnRaw >= 0)
                            yield return this._matrix[startColumnRaw, z];
                        if (endColumnRaw < this._columns)
                            yield return this._matrix[endColumnRaw, z];
                    }
                }
            }
        }

        /// <summary>Gets a range of items</summary>
        /// <param name="fromColumn">From column.</param>
        /// <param name="toColumn">To column.</param>
        /// <param name="fromRow">From row.</param>
        /// <param name="toRow">To row.</param>
        /// <returns>The range of items that lie in between and including the given parameters.</returns>
        public IEnumerable<T> GetRange(
            int fromColumn,
            int toColumn,
            int fromRow,
            int toRow)
        {
            int startColumn = this.AdjustColumnToBounds(fromColumn);
            int endColumn = this.AdjustColumnToBounds(toColumn);
            int startRow = this.AdjustRowToBounds(fromRow);
            int endRow = this.AdjustRowToBounds(toRow);
            for (int x = startColumn; x <= endColumn; ++x)
            {
                for (int z = startRow; z <= endRow; ++z)
                    yield return this._matrix[x, z];
            }
        }

        /// <summary>Gets a range of items</summary>
        /// <param name="fromColumn">From column.</param>
        /// <param name="toColumn">To column.</param>
        /// <param name="fromRow">From row.</param>
        /// <param name="toRow">To row.</param>
        /// <param name="result">The result to be populated by the range of items that lie in between and including the given parameters.</param>
        public void GetRange(
            int fromColumn,
            int toColumn,
            int fromRow,
            int toRow,
            ICollection<T> result)
        {
            int bounds1 = this.AdjustColumnToBounds(fromColumn);
            int bounds2 = this.AdjustColumnToBounds(toColumn);
            int bounds3 = this.AdjustRowToBounds(fromRow);
            int bounds4 = this.AdjustRowToBounds(toRow);
            for (int index1 = bounds1; index1 <= bounds2; ++index1)
            {
                for (int index2 = bounds3; index2 <= bounds4; ++index2)
                    result.Add(this._matrix[index1, index2]);
            }
        }

        /// <summary>Gets a range of items</summary>
        /// <param name="fromColumn">From column.</param>
        /// <param name="toColumn">To column.</param>
        /// <param name="fromRow">From row.</param>
        /// <param name="toRow">To row.</param>
        /// <param name="predicate">A filter predicate, only those elements that match the predicate will be included in the result.</param>
        /// <param name="result">The result to be populated by the range of items that lie in between and including the given parameters.</param>
        public void GetRange(
            int fromColumn,
            int toColumn,
            int fromRow,
            int toRow,
            Func<T, bool> predicate,
            ICollection<T> result)
        {
            int bounds1 = this.AdjustColumnToBounds(fromColumn);
            int bounds2 = this.AdjustColumnToBounds(toColumn);
            int bounds3 = this.AdjustRowToBounds(fromRow);
            int bounds4 = this.AdjustRowToBounds(toRow);
            for (int index1 = bounds1; index1 <= bounds2; ++index1)
            {
                for (int index2 = bounds3; index2 <= bounds4; ++index2)
                {
                    T obj = this._matrix[index1, index2];
                    if (predicate(obj))
                        result.Add(obj);
                }
            }
        }

        /// <summary>Applies an action to a range of items</summary>
        /// <param name="fromColumn">From column.</param>
        /// <param name="toColumn">To column.</param>
        /// <param name="fromRow">From row.</param>
        /// <param name="toRow">To row.</param>
        /// <param name="act">The action to apply. This can be anything from modifying the items to collecting data from the items.</param>
        public void Apply(int fromColumn, int toColumn, int fromRow, int toRow, Action<T> act)
        {
            int bounds1 = this.AdjustColumnToBounds(fromColumn);
            int bounds2 = this.AdjustColumnToBounds(toColumn);
            int bounds3 = this.AdjustRowToBounds(fromRow);
            int bounds4 = this.AdjustRowToBounds(toRow);
            for (int index1 = bounds1; index1 <= bounds2; ++index1)
            {
                for (int index2 = bounds3; index2 <= bounds4; ++index2)
                    act(this._matrix[index1, index2]);
            }
        }

        /// <summary>
        /// Are the matrix indexes within the bounds of the matrix.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="z">The z.</param>
        /// <returns><c>true if in bounds; otherwise false</c></returns>
        protected bool InBounds(int x, int z) => x >= 0 && x <= this._columns - 1 && z >= 0 && z <= this._rows - 1;

        /// <summary>Adjusts the column to bounds.</summary>
        /// <param name="x">The column index.</param>
        /// <returns>The index adjusted to bounds</returns>
        protected int AdjustColumnToBounds(int x)
        {
            if (x < 0)
                return 0;
            return x > this._columns - 1 ? this._columns - 1 : x;
        }

        /// <summary>Adjusts the row to bounds.</summary>
        /// <param name="z">The row index.</param>
        /// <returns>The index adjusted to bounds</returns>
        protected int AdjustRowToBounds(int z)
        {
            if (z < 0)
                return 0;
            return z > this._rows - 1 ? this._rows - 1 : z;
        }
    }
}